package com.xhs.miniapp.api.impl;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.xhs.common.api.XhsConsts;
import com.xhs.common.bean.ToJson;
import com.xhs.common.bean.XhsAccessToken;
import com.xhs.common.enums.XhsType;
import com.xhs.common.error.XhsError;
import com.xhs.common.error.XhsErrorException;
import com.xhs.common.error.XhsRuntimeException;
import com.xhs.common.util.DataUtils;
import com.xhs.common.util.http.RequestExecutor;
import com.xhs.common.util.http.RequestHttp;
import com.xhs.common.util.http.SimpleGetRequestExecutor;
import com.xhs.common.util.http.SimplePostRequestExecutor;
import com.xhs.common.util.json.XhsGsonBuilder;
import com.xhs.miniapp.api.XhsMaService;
import com.xhs.miniapp.bean.XhsMaJscode2SessionResult;
import com.xhs.miniapp.config.XhsMaConfig;
import com.xhs.miniapp.util.XhsMaConfigHolder;
import com.xhs.miniapp.util.crypto.XhsMaSignature;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.function.Function;

/**
 * @see #doGetAccessTokenRequest
 */
@Slf4j
public abstract class BaseXhsMaServiceImpl<H, P> implements XhsMaService, RequestHttp<H, P> {
  /**
   * 开启API签名验证后需要API签名的接口，根据 https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/
   * 整理，uri包含下这些字符串且配置了api signature aes ras key 自动用签名接口
   */
  protected static final String[] urlPathSupportApiSignature =
      new String[] {
//        "cgi-bin/clear_quota",
      };

  protected static final Gson GSON = new Gson();
  private final XhsMaUserService userService = new XhsMaUserServiceImpl(this);

  private Map<String, XhsMaConfig> configMap = new HashMap<>();
  private int retrySleepMillis = 1000;
  private int maxRetryTimes = 5;

  @Override
  public RequestHttp getRequestHttp() {
    return this;
  }

  @Override
  public boolean checkSignature(String timestamp, String nonce, String signature) {
    try {
      return XhsMaSignature.getSignature(this.getWxMaConfig().getToken(), timestamp, nonce).equals(signature);
    } catch (Exception e) {
      log.error("Checking signature failed, and the reason is :" + e.getMessage());
      return false;
    }
  }

  @Override
  public String getAccessToken() throws XhsErrorException {
    return getAccessToken(false);
  }

  @Override
  public String getAccessToken(boolean forceRefresh) throws XhsErrorException {
    if (!forceRefresh && !this.getWxMaConfig().isAccessTokenExpired()) {
      return this.getWxMaConfig().getAccessToken();
    }

    Lock lock = this.getWxMaConfig().getAccessTokenLock();
    boolean locked = false;
    try {
      do {
        locked = lock.tryLock(100, TimeUnit.MILLISECONDS);
        if (!forceRefresh && !this.getWxMaConfig().isAccessTokenExpired()) {
          return this.getWxMaConfig().getAccessToken();
        }
      } while (!locked);

      String response = doGetAccessTokenRequest();
      return extractAccessToken(response);
    } catch (IOException | InterruptedException e) {
      throw new XhsRuntimeException(e);
    } finally {
      if (locked) {
        lock.unlock();
      }
    }
  }

  /**
   * 通过网络请求获取AccessToken
   *
   * @return .
   * @throws IOException .
   */
  protected abstract String doGetAccessTokenRequest() throws IOException;

  @Override
  public String get(String url, String queryParam) throws XhsErrorException {
    return execute(SimpleGetRequestExecutor.create(this), url, queryParam);
  }

  private boolean isApiSignatureRequired(String url) {
    return this.getWxMaConfig().getApiSignatureAesKey() != null
        && Arrays.stream(urlPathSupportApiSignature).anyMatch(part -> url.contains(part));
  }

  @Override
  public String post(String url, String postData) throws XhsErrorException {
//    if (isApiSignatureRequired(url)) {
//      // 接口需要签名
//      log.debug("已经配置接口需要签名，接口{}将走加密访问路径", url);
//      JsonObject jsonObject = GSON.fromJson(postData == null ? "{}" : postData, JsonObject.class);
//      return postWithSignature(url, jsonObject);
//    } else {
      return execute(SimplePostRequestExecutor.create(this), url, postData);
//    }
  }

  @Override
  public String post(String url, Object obj) throws XhsErrorException {
//    if (isApiSignatureRequired(url)) {
//      // 接口需要签名
//      log.debug("已经配置接口需要签名，接口{}将走加密访问路径", url);
//      return postWithSignature(url, obj);
//    } else {
      return this.execute(
          SimplePostRequestExecutor.create(this), url, XhsGsonBuilder.create().toJson(obj));
//    }
  }

  @Override
  public String post(String url, ToJson obj) throws XhsErrorException {
    return this.post(url, obj == null ? "{}" : obj.toJson());
  }

  @Override
  public String post(String url, JsonObject jsonObject) throws XhsErrorException {
    return this.post(url, jsonObject == null ? "{}" : jsonObject.toString());
  }

  /** 向微信端发送请求，在这里执行的策略是当发生access_token过期时才去刷新，然后重新执行请求，而不是全局定时请求 */
  @Override
  public <R, T> R execute(RequestExecutor<R, T> executor, String uri, T data)
      throws XhsErrorException {
    String dataForLog;
    if (data == null) {
      dataForLog = null;
    } else if (data instanceof String) {
      dataForLog = DataUtils.handleDataWithSecret((String) data);
    } else {
      dataForLog = data.toString();
    }
    return executeWithRetry(
        (uriWithAccessToken) -> executor.execute(uriWithAccessToken, data, XhsType.MiniApp),
        uri,
        dataForLog);
  }

  private static interface ExecutorAction<R> {
    R execute(String urlWithAccessToken) throws IOException, XhsErrorException;
  }

  private <R, T> R executeWithRetry(ExecutorAction<R> executor, String uri, String dataForLog)
      throws XhsErrorException {
    int retryTimes = 0;
    do {
      try {
        return this.executeInternal(executor, uri, dataForLog, false);
      } catch (XhsErrorException e) {
        if (retryTimes + 1 > this.maxRetryTimes) {
          log.warn("重试达到最大次数【{}】", maxRetryTimes);
          // 最后一次重试失败后，直接抛出异常，不再等待
          throw new XhsErrorException(
              XhsError.builder()
                  .code(e.getError().getCode())
                  .msg("微信服务端异常，超出重试次数！")
                  .build());
        }

        XhsError error = e.getError();
        // -1 系统繁忙, 1000ms后重试
        if (error.getCode() == -1) {
          int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
          try {
            log.warn("微信系统繁忙，{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
            Thread.sleep(sleepMillis);
          } catch (InterruptedException e1) {
            Thread.currentThread().interrupt();
          }
        } else {
          throw e;
        }
      }
    } while (retryTimes++ < this.maxRetryTimes);

    log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
    throw new XhsRuntimeException("微信服务端异常，超出重试次数");
  }

  private <R, T> R executeInternal(
      ExecutorAction<R> executor, String uri, String dataForLog, boolean doNotAutoRefreshToken)
      throws XhsErrorException {

    if (uri.contains("access_token=")) {
      throw new IllegalArgumentException("uri参数中不允许有access_token: " + uri);
    }
    String accessToken = getAccessToken(false);

    if (StringUtils.isNotEmpty(this.getWxMaConfig().getApiHostUrl())) {
      uri = uri.replace("https://api.weixin.qq.com", this.getWxMaConfig().getApiHostUrl());
    }

    String uriWithAccessToken =
        uri + (uri.contains("?") ? "&" : "?") + "access_token=" + accessToken;
    try {
      R result = executor.execute(uriWithAccessToken);
      log.debug("\n【请求地址】: {}\n【请求参数】：{}\n【响应数据】：{}", uriWithAccessToken, dataForLog, result);
      return result;
    } catch (XhsErrorException e) {
      XhsError error = e.getError();
      if (XhsConsts.ACCESS_TOKEN_ERROR_CODES.contains(error.getCode())) {
        // 强制设置WxMaConfig的access token过期了，这样在下一次请求里就会刷新access token
        Lock lock = this.getWxMaConfig().getAccessTokenLock();
        lock.lock();
        try {
          if (StringUtils.equals(this.getWxMaConfig().getAccessToken(), accessToken)) {
            this.getWxMaConfig().expireAccessToken();
          }
        } catch (Exception ex) {
          this.getWxMaConfig().expireAccessToken();
        } finally {
          lock.unlock();
        }
        if (this.getWxMaConfig().autoRefreshToken() && !doNotAutoRefreshToken) {
          log.warn(
              "即将重新获取新的access_token，错误代码：{}，错误信息：{}", error.getCode(), error.getMsg());
          // 下一次不再自动重试
          // 当小程序误调用第三方平台专属接口时,第三方无法使用小程序的access token,如果可以继续自动获取token会导致无限循环重试,直到栈溢出
          return this.executeInternal(executor, uri, dataForLog, true);
        }
      }

      if (error.getCode() != 0) {
        log.warn("\n【请求地址】: {}\n【请求参数】：{}\n【错误信息】：{}", uriWithAccessToken, dataForLog, error);
        throw new XhsErrorException(error, e);
      }
      return null;
    } catch (IOException e) {
      log.warn(
          "\n【请求地址】: {}\n【请求参数】：{}\n【异常信息】：{}", uriWithAccessToken, dataForLog, e.getMessage());
      throw new XhsRuntimeException(e);
    }
  }

  /**
   * 设置当前的AccessToken
   *
   * @param resultContent 响应内容
   * @return access token
   * @throws XhsErrorException 异常
   */
  protected String extractAccessToken(String resultContent) throws XhsErrorException {
    log.debug("access-token response: {}", resultContent);
    XhsMaConfig config = this.getWxMaConfig();
    XhsError error = XhsError.fromJson(resultContent, XhsType.MiniApp);
    if (error.getCode() != 0) {
      throw new XhsErrorException(error);
    }
    XhsAccessToken accessToken = XhsAccessToken.fromJson(error.getData());
    config.updateAccessToken(accessToken);
//    config.updateAccessTokenProcessor(accessToken.getAccessToken(), accessToken.getExpiresIn());
    return accessToken.getAccessToken();
  }

  @Override
  public XhsMaConfig getWxMaConfig() {
    if (this.configMap.size() == 1) {
      // 只有一个小程序，直接返回其配置即可
      return this.configMap.values().iterator().next();
    }

    return this.configMap.get(XhsMaConfigHolder.get());
  }

  @Override
  public void setWxMaConfig(XhsMaConfig maConfig) {
    final String appid = maConfig.getAppid();
    this.setMultiConfigs(ImmutableMap.of(appid, maConfig), appid);
  }

  @Override
  public void setMultiConfigs(Map<String, XhsMaConfig> configs) {
    this.setMultiConfigs(configs, configs.keySet().iterator().next());
  }

  @Override
  @JsonDeserialize
  public void setMultiConfigs(Map<String, XhsMaConfig> configs, String defaultMiniappId) {
    // 防止覆盖配置
    if (this.configMap != null) {
      this.configMap.putAll(configs);
    } else {
      this.configMap = Maps.newHashMap(configs);
    }
    XhsMaConfigHolder.set(defaultMiniappId);
    this.initHttp();
  }

  @Override
  public void addConfig(String miniappId, XhsMaConfig configStorages) {
    synchronized (this) {
      /*
       * 因为commit f74b00cf 默认初始化了configMap，导致使用此方法无法进入if从而触发initHttp()，
       * 就会出现HttpClient报NullPointException
       */
      if (this.configMap == null || this.configMap.isEmpty()) {
        this.setWxMaConfig(configStorages);
      } else {
        XhsMaConfigHolder.set(miniappId);
        this.configMap.put(miniappId, configStorages);
      }
    }
  }

  @Override
  public void removeConfig(String miniappId) {
    synchronized (this) {
      if (this.configMap.size() == 1) {
        this.configMap.remove(miniappId);
        log.warn("已删除最后一个小程序配置：{}，须立即使用setWxMaConfig或setMultiConfigs添加配置", miniappId);
        return;
      }
      if (XhsMaConfigHolder.get().equals(miniappId)) {
        this.configMap.remove(miniappId);
        final String defaultMpId = this.configMap.keySet().iterator().next();
        XhsMaConfigHolder.set(defaultMpId);
        log.warn("已删除默认小程序配置，小程序【{}】被设为默认配置", defaultMpId);
        return;
      }
      this.configMap.remove(miniappId);
    }
  }

  @Override
  public XhsMaService switchoverTo(String miniAppId) {
    return switchoverTo(miniAppId, null);
  }

  @Override
  public XhsMaService switchoverTo(String miniAppId, Function<String, XhsMaConfig> func) {
    if (this.configMap.containsKey(miniAppId)) {
      XhsMaConfigHolder.set(miniAppId);
      return this;
    }

    if (func != null) {
      XhsMaConfig config = func.apply(miniAppId);
      if (config != null) {
        this.addConfig(miniAppId, config);
        return this;
      }
    }

    throw new XhsRuntimeException(String.format("无法找到对应【%s】的小程序配置信息，请核实！", miniAppId));
  }

  @Override
  public boolean switchover(String mpId) {
    if (this.configMap.containsKey(mpId)) {
      XhsMaConfigHolder.set(mpId);
      return true;
    }

    log.error("无法找到对应【{}】的小程序配置信息，请核实！", mpId);
    return false;
  }

  @Override
  public void setRetrySleepMillis(int retrySleepMillis) {
    this.retrySleepMillis = retrySleepMillis;
  }

  @Override
  public void setMaxRetryTimes(int maxRetryTimes) {
    this.maxRetryTimes = maxRetryTimes;
  }

  @Override
  public XhsMaUserService getUserService() {
    return this.userService;
  }

  @Override
  public XhsMaJscode2SessionResult jsCode2SessionInfo(String jsCode) throws XhsErrorException {
    final XhsMaConfig config = getWxMaConfig();
    Map<String, String> params = new HashMap<>(8);
    params.put("appid", config.getAppid());
    params.put("access_token", getAccessToken(false));
    params.put("code", jsCode);

    String result =
            get(JSCODE_TO_SESSION_URL, Joiner.on("&").withKeyValueSeparator("=").join(params));
    String data = XhsError.fromJson(result).getData();
    return XhsMaJscode2SessionResult.fromJson(data);
  }

}
